﻿using Grand.Business.Core.Interfaces.Catalog.Products;
using Grand.Business.Core.Queries.Catalog;
using Grand.Data;
using Grand.Domain;
using Grand.Domain.Catalog;
using Grand.Domain.Customers;
using Grand.Infrastructure.Configuration;
using MediatR;

namespace Grand.Business.Catalog.Queries.Handlers;

public class GetSearchProductsQueryHandler : IRequestHandler<GetSearchProductsQuery, (IPagedList<Product> products,
    IList<string> filterableSpecificationAttributeOptionIds)>
{
    private readonly AccessControlConfig _accessControlConfig;
    private readonly CatalogSettings _catalogSettings;
    private readonly IRepository<Product> _productRepository;
    private readonly ISpecificationAttributeService _specificationAttributeService;

    public GetSearchProductsQueryHandler(
        IRepository<Product> productRepository,
        ISpecificationAttributeService specificationAttributeService,
        CatalogSettings catalogSettings,
        AccessControlConfig accessControlConfig)
    {
        _productRepository = productRepository;
        _specificationAttributeService = specificationAttributeService;
        _catalogSettings = catalogSettings;
        _accessControlConfig = accessControlConfig;
    }

    public async Task<(IPagedList<Product> products, IList<string> filterableSpecificationAttributeOptionIds)>
        Handle(GetSearchProductsQuery request, CancellationToken cancellationToken)
    {
        var filterableSpecificationAttributeOptionIds = new List<string>();

        //validate "categoryIds" parameter
        if (request.CategoryIds != null && request.CategoryIds.Contains(""))
            request.CategoryIds.Remove("");

        //Access control list. Allowed customer groups
        var allowedCustomerGroupsIds = request.Customer.GetCustomerGroupIds();

        #region Search products

        //products
        var query = from p in _productRepository.Table
            select p;

        query = FilterQueryable(request, query, allowedCustomerGroupsIds);

        var querySpecification = query;

        //search by specs
        if (request.FilteredSpecs != null && request.FilteredSpecs.Any())
        {
            var spec = new HashSet<string>();
            var dictionary = new Dictionary<string, List<string>>();
            foreach (var key in request.FilteredSpecs)
            {
                var specification = await _specificationAttributeService.GetSpecificationAttributeByOptionId(key);
                if (specification == null) continue;
                spec.Add(specification.Id);
                if (!dictionary.ContainsKey(specification.Id))
                {
                    //add
                    dictionary.Add(specification.Id, new List<string>());
                    querySpecification = querySpecification.Where(x =>
                        x.ProductSpecificationAttributes.Any(y =>
                            y.SpecificationAttributeId == specification.Id && y.AllowFiltering));
                }

                dictionary[specification.Id].Add(key);
            }

            foreach (var item in dictionary)
                query = query.Where(x => x.ProductSpecificationAttributes.Any(y =>
                    y.SpecificationAttributeId == item.Key && y.AllowFiltering
                                                           && item.Value.Contains(
                                                               y.SpecificationAttributeOptionId)));
        }

        if (request.SpecificationOptions != null && request.SpecificationOptions.Any())
            query = query.Where(x => x.ProductSpecificationAttributes.Any(y =>
                request.SpecificationOptions.Contains(y.SpecificationAttributeOptionId)));

        query = OrderByQueryable(request, query);

        var products = await PagedList<Product>.Create(query, request.PageIndex, request.PageSize);

        if (!request.LoadFilterableSpecificationAttributeOptionIds ||
            _catalogSettings.IgnoreFilterableSpecAttributeOption)
            return (products, filterableSpecificationAttributeOptionIds);
        {
            var filterSpecExists =
                querySpecification.Where(x => x.ProductSpecificationAttributes.Any(x => x.AllowFiltering));

            var spec = from p in filterSpecExists
                from item in p.ProductSpecificationAttributes
                select item;

            var groupQuerySpec = spec.Where(x => x.AllowFiltering).GroupBy(x =>
                new { x.SpecificationAttributeOptionId }).ToList();
            IList<string> specification =
                groupQuerySpec.Select(item => item.Key.SpecificationAttributeOptionId).ToList();

            filterableSpecificationAttributeOptionIds = specification.ToList();
        }

        return (products, filterableSpecificationAttributeOptionIds);

        #endregion
    }

    private IQueryable<Product> FilterQueryable(GetSearchProductsQuery request, IQueryable<Product> query,
        string[] allowedCustomerGroupsIds)
    {
        //category filtering
        if (request.CategoryIds != null && request.CategoryIds.Any())
        {
            if (request.FeaturedProducts.HasValue)
                query = query.Where(x => x.ProductCategories.Any(y => request.CategoryIds.Contains(y.CategoryId)
                                                                      && y.IsFeaturedProduct ==
                                                                      request.FeaturedProducts));
            else
                query = query.Where(x => x.ProductCategories.Any(y => request.CategoryIds.Contains(y.CategoryId)));
        }

        //brand
        if (!string.IsNullOrEmpty(request.BrandId)) query = query.Where(x => x.BrandId == request.BrandId);

        //collection filtering
        if (!string.IsNullOrEmpty(request.CollectionId))
        {
            if (request.FeaturedProducts.HasValue)
                query = query.Where(x => x.ProductCollections.Any(y => y.CollectionId == request.CollectionId
                                                                       && y.IsFeaturedProduct ==
                                                                       request.FeaturedProducts));
            else
                query = query.Where(x => x.ProductCollections.Any(y => y.CollectionId == request.CollectionId));
        }

        if (!request.OverridePublished.HasValue)
        {
            //process according to "showHidden"
            if (!request.ShowHidden) query = query.Where(p => p.Published);
        }
        else if (request.OverridePublished.Value)
        {
            //published only
            query = query.Where(p => p.Published);
        }
        else if (!request.OverridePublished.Value)
        {
            //unpublished only
            query = query.Where(p => !p.Published);
        }

        if (request.VisibleIndividuallyOnly) query = query.Where(p => p.VisibleIndividually);

        if (request.ProductType.HasValue) query = query.Where(p => p.ProductTypeId == request.ProductType);

        if (request.ShowOnHomePage.HasValue) query = query.Where(p => p.ShowOnHomePage == request.ShowOnHomePage.Value);

        var nowUtc = DateTime.UtcNow;
        if (request.PriceMin.HasValue) query = query.Where(p => p.Price >= request.PriceMin.Value);

        if (request.PriceMax.HasValue)
            //max price
            query = query.Where(p => p.Price <= request.PriceMax.Value);

        if (request.Rating.HasValue)
            //avg rating
            query = query.Where(p => p.AvgRating >= request.Rating);

        if (!request.ShowHidden && !_catalogSettings.IgnoreFilterableAvailableStartEndDateTime)
            query = query.Where(p =>
                (p.AvailableStartDateTimeUtc == null || p.AvailableStartDateTimeUtc < nowUtc) &&
                (p.AvailableEndDateTimeUtc == null || p.AvailableEndDateTimeUtc > nowUtc));

        if (request.MarkedAsNewOnly)
        {
            query = query.Where(p => p.MarkAsNew);
            query = query.Where(p =>
                (!p.MarkAsNewStartDateTimeUtc.HasValue || p.MarkAsNewStartDateTimeUtc.Value < nowUtc) &&
                (!p.MarkAsNewEndDateTimeUtc.HasValue || p.MarkAsNewEndDateTimeUtc.Value > nowUtc));
        }

        //searching by keyword
        if (!string.IsNullOrWhiteSpace(request.Keywords))
        {
            if (!request.SearchDescriptions)
                query = query.Where(p =>
                    p.Name.ToLower().Contains(request.Keywords.ToLower())
                    ||
                    p.Locales.Any(x =>
                        x.LocaleKey == "Name" && x.LocaleValue != null &&
                        x.LocaleValue.ToLower().Contains(request.Keywords.ToLower()))
                    ||
                    (request.SearchSku && p.Sku != null && p.Sku.ToLower().Contains(request.Keywords.ToLower()))
                );
            else
                query = query.Where(p =>
                    (p.Name != null && p.Name.ToLower().Contains(request.Keywords.ToLower()))
                    ||
                    (p.ShortDescription != null &&
                     p.ShortDescription.ToLower().Contains(request.Keywords.ToLower()))
                    ||
                    (p.FullDescription != null && p.FullDescription.ToLower().Contains(request.Keywords.ToLower()))
                    ||
                    p.Locales.Any(x =>
                        x.LocaleValue != null && x.LocaleValue.ToLower().Contains(request.Keywords.ToLower()))
                    ||
                    (request.SearchSku && p.Sku != null && p.Sku.ToLower().Contains(request.Keywords.ToLower()))
                );
        }

        if (!request.ShowHidden && !_accessControlConfig.IgnoreAcl)
            query = from p in query
                where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x))
                select p;

        if (!string.IsNullOrEmpty(request.StoreId) && !_accessControlConfig.IgnoreStoreLimitations)
            query = query.Where(x => x.Stores.Any(y => y == request.StoreId) || !x.LimitedToStores);

        //vendor filtering
        if (!string.IsNullOrEmpty(request.VendorId)) query = query.Where(x => x.VendorId == request.VendorId);

        //warehouse filtering
        if (!string.IsNullOrEmpty(request.WarehouseId))
            query = query.Where(x =>
                (x.UseMultipleWarehouses &&
                 x.ProductWarehouseInventory.Any(y => y.WarehouseId == request.WarehouseId))
                || (!x.UseMultipleWarehouses && x.WarehouseId == request.WarehouseId));

        //tag filtering
        if (!string.IsNullOrEmpty(request.ProductTag))
            query = query.Where(x => x.ProductTags.Any(y => y == request.ProductTag));

        return query;
    }

    private IQueryable<Product> OrderByQueryable(GetSearchProductsQuery request, IQueryable<Product> query)
    {
        switch (request.OrderBy)
        {
            case ProductSortingEnum.Position when request.CategoryIds != null && request.CategoryIds.Any():
                //category position
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenBy(x => x.DisplayOrderCategory)
                    : query.OrderBy(x => x.DisplayOrderCategory);
                break;
            case ProductSortingEnum.Position when !string.IsNullOrEmpty(request.BrandId):
                //brand position
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenBy(x => x.DisplayOrderBrand)
                    : query.OrderBy(x => x.DisplayOrderBrand);
                break;
            case ProductSortingEnum.Position when !string.IsNullOrEmpty(request.CollectionId):
                //collection position
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenBy(x => x.DisplayOrderCollection)
                    : query.OrderBy(x => x.DisplayOrderCollection);
                break;
            case ProductSortingEnum.Position:
                //otherwise sort by name
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenBy(x => x.Name)
                    : query.OrderBy(x => x.Name);
                break;
            case ProductSortingEnum.NameAsc:
                //Name: A to Z
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenBy(x => x.Name)
                    : query.OrderBy(x => x.Name);
                break;
            case ProductSortingEnum.NameDesc:
                //Name: Z to A
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenByDescending(x => x.Name)
                    : query.OrderByDescending(x => x.Name);
                break;
            case ProductSortingEnum.PriceAsc:
                //Price: Low to High
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenBy(x => x.Price)
                    : query.OrderBy(x => x.Price);
                break;
            case ProductSortingEnum.PriceDesc:
                //Price: High to Low
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenByDescending(x => x.Price)
                    : query.OrderByDescending(x => x.Price);
                break;
            case ProductSortingEnum.CreatedOn:
                //creation date
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenBy(x => x.CreatedOnUtc)
                    : query.OrderBy(x => x.CreatedOnUtc);
                break;
            case ProductSortingEnum.OnSale:
                //on sale
                query = query.OrderBy(x => x.OnSale);

                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenBy(x => x.OnSale)
                    : query.OrderBy(x => x.OnSale);
                break;
            case ProductSortingEnum.MostViewed:
                //most viewed
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenByDescending(x => x.Viewed)
                    : query.OrderByDescending(x => x.Viewed);
                break;
            case ProductSortingEnum.BestSellers:
                //best seller
                query = query.OrderByDescending(x => x.Sold);

                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenByDescending(x => x.Sold)
                    : query.OrderByDescending(x => x.Sold);
                break;
            case ProductSortingEnum.Rating:
                //most viewed
                query = _catalogSettings.SortingByAvailability
                    ? query.OrderBy(x => x.LowStock).ThenByDescending(x => x.AvgRating)
                    : query.OrderByDescending(x => x.AvgRating).ThenByDescending(x => x.ApprovedTotalReviews);
                break;
        }

        return query;
    }
}